package worker

import (
	"GoCrontab/crontab/common"
	"fmt"
	"strconv"
	"time"
)

// 任务调度协程，需要判断事件到期和事件处理
type Scheduler struct {
	//chan里方任务事件,所有的put和delete事件都在这里
	JobEventChan chan *common.JobEvent
	//调度计划表，key 为名字，名字全局唯一，记录执行时间，下次执行命令
	JobPlanTable map[string]*common.JobSchedulePlan
	//任务执行表,正在执行的表放在这里，可以通过这个表来判断当前任务是否正在执行。在这个表里，不执行，不在这个表里，执行。
	JobExecutingTable map[string]*common.JobExecuteInfo
	//任务结果队列
	jobResultChan chan *common.JobExecuteResult
}

var (
	GlobalScheduler *Scheduler
)

//重要的是处理任务【save |delete |kill】，维护map任务列表，通过etcd发来的任务事件，将planMap和etcd保持一致
func (s *Scheduler) HandleJobEvent(jobEvent *common.JobEvent) (err error) {
	var (
		jobPlan        *common.JobSchedulePlan
		jobExisted     bool
		jobExecuteInfo *common.JobExecuteInfo
		jobExecuting   bool
	)
	fmt.Println("接收到事件，" + strconv.Itoa(jobEvent.EventType))
	switch jobEvent.EventType {
	case common.EVENT_JOB_SAVE: //save 事件
		fmt.Println("hi,接收到save事件，" + strconv.Itoa(jobEvent.EventType))
		//传递job过去，返回具体的调度时间和执行相关（主要是parse cron）
		if jobPlan, err = common.BuildJobSchedulerPlan(jobEvent.Job); err != nil {
			fmt.Println("HandleJobEvent发生了 什么", err.Error())
			return
		}
		//扔到map里
		fmt.Println("接收到save事件，" + strconv.Itoa(jobEvent.EventType))
		s.JobPlanTable[jobEvent.Job.Name] = jobPlan
	//@todo 通过开源库传递cron表达式，拿到下次的执行时间
	//jobEvent.Job.CronExpr
	case common.EVENT_JOB_DELETE: //delete 事件
		//如果etcd中没有任务存在，但是你调用delete的时候，还会有delete的事件推送过来，所以这里需要判断 通过ok-idiom
		if jobPlan, jobExisted = s.JobPlanTable[jobEvent.Job.Name]; jobExisted {
			delete(s.JobPlanTable, jobEvent.Job.Name) //有的话删除
		}

	//任务删除事件，需要调用job的cancelFuc
	case common.JOB_EVENT_KILL:
		//取消掉command执行，判断任务是否在执行中.这个返回结果有具体执行状态？
		if jobExecuteInfo, jobExecuting = s.JobExecutingTable[jobEvent.Job.Name]; jobExecuting {
			jobExecuteInfo.CancelFunc()
		}

	}
	return
}

//
func (s *Scheduler) TrySchedule() (schedulerAfter time.Duration) {
	var (
		now      time.Time
		jobPlan  *common.JobSchedulePlan
		nearTime *time.Time
	)

	//如果计划表里没有任务，睡眠1秒
	if len(s.JobPlanTable) == 0 {
		schedulerAfter = 1 * time.Second
		fmt.Println("JobPlanTable里没数据,返回")
		return
	}

	now = time.Now()
	//遍历所有任务
	for _, jobPlan = range s.JobPlanTable {
		//单个任务的执行时间过去 或者和当前时间相等
		if jobPlan.NextExecTime.Before(now) || jobPlan.NextExecTime.Equal(now) {
			//执行任务
			s.TryStartExecJob(jobPlan)
			//传递当前时间，得到下次执行时间
			jobPlan.NextExecTime = jobPlan.Expr.Next(now)
		}

		//统计最近一个要过期的任务时间
		if nearTime == nil || jobPlan.NextExecTime.Before(*nearTime) {
			nearTime = &jobPlan.NextExecTime
		}
	}
	//下次调度时间
	schedulerAfter = (*nearTime).Sub(now)
	return
}

//调度协程，判断事件是否过期
func (s *Scheduler) schedulerLoop() (err error) {
	var (
		jobEvent      *common.JobEvent
		scheduleAfter time.Duration
		scheduleTimer *time.Timer

		jobResult *common.JobExecuteResult
	)
	//调度之前先初始化
	scheduleAfter = s.TrySchedule()
	scheduleTimer = time.NewTimer(scheduleAfter)
	//	检测任务是否过期，定时任务是基于corn表达式来计算下次执行时间
	//for 循环一个任务事件，监听来自JobEventChan中的JobEvent事件，当有任务来的时候
	//1. 拉取所有 2. put 3. delete
	for {
		select {
		//所有任务的增删改查都会在这里实时体现，
		case jobEvent = <-s.JobEventChan:
			//对任务列表做增删改查（在map内存中）
			if err = s.HandleJobEvent(jobEvent); err != nil {
				fmt.Println("schedulerLoop中发现err", err)
				return
			}
			fmt.Println("变了变了", jobEvent.Job.Name)
		case <-scheduleTimer.C: //最近的任务到期了
		//注意没有任务不能打印job.name,否则会报
		//fmt.Println(jobEvent.Job.Name, "最近的任务到期了")

		case jobResult = <-s.jobResultChan: //任务的执行结果，通过exec写入channel
			fmt.Println(jobResult.ExecuteInfo.Job.Name, " 执行完了")
			fmt.Println("==============", time.Now().Format("2006-01-02 15:04:05"))
			s.HandleJobResult(jobResult)
		}
		//调度一次任务
		scheduleAfter = s.TrySchedule()
		// 重置调度间隔
		scheduleTimer.Reset(scheduleAfter)
	}

}

//接受来自worker的jobManager推过来的数据，然后放到chan里
func (s *Scheduler) PushJobEvent(jobEvent *common.JobEvent) {
	s.JobEventChan <- jobEvent
}

//尝试执行任务，注意理解调度和执行是两码事,调度是定期的检查哪些任务过期了，哪些需要执行。然后再执行
func (s *Scheduler) TryStartExecJob(jobPlan *common.JobSchedulePlan) {
	var (
		//任务执行的几种状态
		jobExecuteInfo *common.JobExecuteInfo
		//任务是否在执行
		jobExecuting bool
	)
	//执行的任务可能运行很久，1分钟会调度60次，但是只能执行一次，防止并发操作。注意理解上面的调度和执行。所以这里是try try try
	//执行之前检测一下订单状态，如果任务正在执行，需要跳出本次调度
	if jobExecuteInfo, jobExecuting = s.JobExecutingTable[jobPlan.Job.Name]; jobExecuting {
		fmt.Println("当前job:正在执行 " + jobPlan.Job.Name)
		return
	}
	//构建执行状态信息
	jobExecuteInfo = common.BuildJobExecuteInfo(jobPlan)

	fmt.Println("what happend", jobExecuteInfo)
	//保存执行状态，进行对key的赋值
	s.JobExecutingTable[jobPlan.Job.Name] = jobExecuteInfo
	//执行任务，实际就是启动一个shell命令，执行
	fmt.Sprintf("执行任务:%s,计划执行时间 %s,实际执行时间%s", jobPlan.Job.Name, jobExecuteInfo.PlanExecTime, jobExecuteInfo.RealExecTime)
	GlobalExecutor.ExecuteJob(jobExecuteInfo)
}

//executor执行完毕后，将结果推给scheduler,
func (s *Scheduler) PushJobResult(result *common.JobExecuteResult) {
	fmt.Println("开始推送执行结果,任务名： ", result.ExecuteInfo.Job.Name)
	s.jobResultChan <- result
	fmt.Println("推送执行结果成功,任务名： ", result.ExecuteInfo.Job.Name)
}

//处理任务执行结果，需要删除在executing 表里的数据，写入处理日志
func (s *Scheduler) HandleJobResult(result *common.JobExecuteResult) {

	var (
		jobLog *common.JobLog
	)
	//执行完了就从执行的ing表中删除，要不然会以为一直执行
	delete(s.JobExecutingTable, result.ExecuteInfo.Job.Name)
	fmt.Println(result.ExecuteInfo.Job.Name, "执行完了，从正在执行的map里删除")

	//代表执行成功，写入执行日志
	if result.Err != common.ERR_LOCK_ALREADY_REQUIRED {

		jobLog = &common.JobLog{
			JobName:      result.ExecuteInfo.Job.Name,
			Command:      result.ExecuteInfo.Job.Command,
			Output:       string(result.Output),
			PlanExecTime: result.ExecuteInfo.PlanExecTime.UnixNano(),
			ScheduleTime: result.ExecuteInfo.RealExecTime.UnixNano(),
			RealExecTime: result.StartTime.UnixNano(),
			EndTime:      result.EndTime.UnixNano(),
		}
		//有值赋值为string
		if result.Err != nil {
			jobLog.Err = result.Err.Error()
		} else {
			jobLog.Err = ""
		}
		GlobalLogSink.Append(jobLog)
	}
}

func InitScheduler() (err error) {
	//初始化全局,注意是初始化（make）chan,这里的1000是1000条job上限？
	GlobalScheduler = &Scheduler{
		//Scheduler watch所有的事件，然后保存到PlanTable的map里
		JobEventChan:      make(chan *common.JobEvent, 1000),
		JobPlanTable:      make(map[string]*common.JobSchedulePlan, 1000),
		jobResultChan:     make(chan *common.JobExecuteResult, 1000),
		JobExecutingTable: make(map[string]*common.JobExecuteInfo, 1000),
	}

	go GlobalScheduler.schedulerLoop()

	return
}
